package com.accedo.achievements.dataaccess.dao.impl;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.accedo.achievements.dataaccess.dao.UserDao;
import com.accedo.achievements.dataaccess.model.Achievement;
import com.accedo.achievements.dataaccess.model.Badge;
import com.accedo.achievements.dataaccess.model.Campaign;
import com.accedo.achievements.dataaccess.model.User;

/**
 * A DAO class with user model database operations.
 * @author Bo Liang
 */
@Component
public class UserDaoImpl implements UserDao {

	private static final Logger log = LoggerFactory.getLogger(UserDaoImpl.class);
	
	private SessionFactory sessionFactory; // injection
	private User user; // injection
	private Achievement achi; //injection
	private Badge badge; //injection
	private Campaign camp; //injection
	
	// Constructor
	UserDaoImpl() {}; 
	
	/**
	 * Get user object by using user id.
	 * @param user_id	user id
	 * @return			user object
	 */
	public User getUserByUserId(int user_id) {
		log.debug("Entering getUserByUserId(user_id = {})", user_id);
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			Query query = session.createQuery("from User user where user.id = :user_id")
					.setParameter("user_id", user_id); 
			user = (User) query.uniqueResult(); 
		} catch (Exception e) {
			e.printStackTrace();
			log.error("getUserByUserId Catch Exception: {}", e.getMessage());
		} finally {
			session.close();
		}	
		
		log.debug("Leaving getUserByUserId(): username = {}", user.getUsername());
		return user;
	}
	
	/**
	 * Get friends' id List by using user id.
	 * @param user_id	user id
	 * @return			List of friends' id
	 */
	@SuppressWarnings("unchecked")
	public List<Integer> getFriendIdListByUserId(int user_id) {
		log.debug("Entering getFriendIdListByUserId(user_id = {})", user_id);
		
		List<Integer> fri_idList = new ArrayList<Integer>(); 
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			Query query = session.createSQLQuery("select fri_id from t_user_friend where user_id = :user_id")
			   .setParameter("user_id", user_id);
			fri_idList = query.list(); 
		} catch (Exception e) {
			e.printStackTrace();
			log.error("getFriendIdListByUserId Catch Exception: {}", e.getMessage());
		} finally {
			session.close();
		}
		
		log.debug("Leaving getFriendIdListByUserId(): fri_idList.size = {}", fri_idList.size());
		return fri_idList; 
	}
	
	/**
	 * Add achievement to user when user unlock this achievement.
	 * <p>user may unlocked this achievement before, if so, just add 1 to the number of unlocked of this achievement,
	 * if user is the first time unlock this achievement, add achievement to user and return flag variable with "new",
	 * which means make the program to do the following work related badge and campaign.
	 * @param username	user name
	 * @param achi_sn	achievement serial number
	 * @return 			string flag : new/old whether the first time unlock the achievement
	 */
	public String addAchiToUser(String username, String achi_sn) {
		log.debug("Entering addAchiToUser(username = {}, achi_sn = {})", username, achi_sn);
		String flag = null;
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			session.beginTransaction();
			
			// get achi_id, app_sn and user_id by user name and achi_sn
			Query query = session.createQuery("from Achievement achi where achi.achi_sn = :achi_sn")
					.setParameter("achi_sn", achi_sn); 
			achi = (Achievement) query.uniqueResult(); 
			int achi_id = achi.getId(); 
			String app_sn = achi.getApp_sn(); 
			
			// get user_id by user name
			query = session.createQuery("from User user where user.username = :username")
					.setParameter("username", username); 
			user = (User) query.uniqueResult(); 
			int user_id = user.getId(); 
			
			// if this user has already unlocked this achievement before, just make achi_num plus 1 
			if (user.getAchis().contains(achi)) {
				session.createSQLQuery("update t_user_achievement set achi_num=achi_num+1, app_sn = :app_sn where user_id = :user_id and achi_id = :achi_id")
					   .setParameter("app_sn", app_sn)
				       .setParameter("user_id", user_id)
				       .setParameter("achi_id", achi_id)
				       .executeUpdate();
				session.getTransaction().commit();
				// set flag to "old" means user has unlocked this achievement before
				flag = "old"; 
				
			/**
			 *  if this user has not unlocked this achievement before,
			 *  Firstly, add this achievement into this user,
			 *  Secondly, update this piece of record with the app_sn which this achievement was belonged to.
			 */ 
			} else {
				user.getAchis().add(achi);
				session.save(user);
				session.getTransaction().commit();
				
				session.createSQLQuery("update t_user_achievement set app_sn = :app_sn where user_id = :user_id and achi_id = :achi_id")
				   .setParameter("app_sn", app_sn)
				   .setParameter("user_id", user_id)
				   .setParameter("achi_id", achi_id)
			       .executeUpdate();
				// only return flag with "new", program will continue to do following work related badge and campaign
				flag = "new"; 
			}	
		} catch (Exception e) {
			e.printStackTrace();
			log.error("addAchiToUser Catch Exception: {}", e.getMessage());
		} finally {
			session.close();
		}
		log.debug("Leaving addAchiToUser(): flag = {}", flag);
		return flag; 
	}
	
	/**
	 * Add badge object to the user objcet's property badge Set.
	 * <p>it does not meant this user unlocked this badge,
	 * when user unlocked one achievement that related this badge 
	 * and in the same time this user have not unlocked any other achievements related to this badge,
	 * system will call this function to create the relationship of this user and badge. 
	 * @param username 	user name
	 * @param badge_sn 	badge serial number
	 * @return			true/false whether successfully add badge to user
	 */
	public boolean addBadgeToUser(String username, String badge_sn) {
		log.debug("Entering addBadgeToUser(username = {}, badge_sn = {})", username, badge_sn);
		Session session = null; 
		Set<Badge> badges = new HashSet<Badge>(); 
		try {
//			session = SessionFactoryUtil.getSessionFactory().openSession(); 
			session = sessionFactory.openSession();
			session.beginTransaction();
			
			// get the user object by using username
			Query query = session.createQuery("from User user where user.username = :username")
						  .setParameter("username", username); 
			user = (User) query.uniqueResult(); 
			
			// get the badge object by using badge_sn
			query = session.createQuery("from Badge badge where badge.badge_sn = :badge_sn")
					.setParameter("badge_sn", badge_sn); 
			badge = (Badge)query.uniqueResult(); 
			
			// add the badge to user
			badges = user.getBadges(); 
			badges.add(badge);
			user.setBadges(badges); 
			
			session.save(user); 
			session.getTransaction().commit();
			
		} catch (Exception e) {
			e.printStackTrace();
			session.getTransaction().rollback();
			log.error("addBadgeToUser Catch Exception: {}", e.getMessage());
			return false;
		} finally {
//			SessionFactoryUtil.closeSession(session);
			session.close();
		}
		
		log.debug("Leaving addBadgeToUser(): true");
		return true;
	}
	
	/**
	 * Increase the number of achievements of the specific badge, which achievement that user just unlocked.
	 * <p>in user_badge relationship table, two fields "badge_total" and "badge_num" that the first one stored the
	 * achievements number this badge has, and the second one is the quantity of achievements that this user unlocked.
	 * this function is invoked after user triggered one achievement first time and add the number plus one to the table's
	 * "badge_num" field and also has functionality to judge whether "badge_total" equals to "badge_num", if so, make the
	 * unlock_flag to 1, which means this user obtained the badge.
	 * @param username	user name
	 * @param badge_id	badge id
	 * @return			true/false
	 */
	public boolean addAchiUnlockedNumForUserBadge(String username, int badge_id) {
		log.debug("Entering addAchiUnlockedNumForUserBadge(username = {}, badge_id = {})", username, badge_id);
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			session.beginTransaction();
			// get badge object by using badge id
			Query query = session.createQuery("from Badge badge where badge.id = :badge_id")
					.setParameter("badge_id", badge_id); 
			badge = (Badge) query.uniqueResult();   
			
			// get user object and user id by using user name
			query = session.createQuery("from User user where user.username = :username")
					.setParameter("username", username); 
			user = (User) query.uniqueResult(); 
			int user_id = user.getId(); 
			
			// if user already unlock achievements related this badge before, just make badge_num add 1
			if (user.getBadges().contains(badge)) {
				session.createSQLQuery("update t_user_badge set badge_num=badge_num+1 where user_id = :user_id and badge_id = :badge_id")
				       .setParameter("user_id", user_id)
				       .setParameter("badge_id", badge_id)
				       .executeUpdate();
				session.getTransaction().commit();
			// if user does not have this badge record, add a new badge
			} else {
				user.getBadges().add(badge);
				session.save(user);
				session.getTransaction().commit();
				
				// count achievement quantity of this badge and assign this value to badge_total field
				query = session.createSQLQuery("select count(achi_id) from t_badge_achievement where badge_id = :badge_id")
							   .setParameter("badge_id", badge.getId()); 
				int badge_total = ((BigInteger) query.uniqueResult()).intValue(); 
				
				session.createSQLQuery("update t_user_badge set badge_total = :badge_total where user_id = :user_id and badge_id = :badge_id")
			       	   .setParameter("badge_total", badge_total)
					   .setParameter("user_id", user_id)
			           .setParameter("badge_id", badge_id)
			           .executeUpdate();
				
			}
			// if badge_total equals badge_num, it means this user unlocked this badge, and then assign the unlock_flag to 1
			session.createSQLQuery("update t_user_badge set unlock_flag = 1 where badge_total = badge_num and user_id = :user_id and badge_id = :badge_id")
		       .setParameter("user_id", user_id)
		       .setParameter("badge_id", badge_id)
		       .executeUpdate();

		} catch (Exception e) {
			e.printStackTrace();
			session.getTransaction().rollback();
			log.error("addAchiUnlockedNumForUserBadge Catch Exception: {}", e.getMessage());
			return false;
		} finally {
			session.close();
		}
		
		log.debug("Leaving addAchiUnlockedNumForUserBadge(): true");
		return true; 
	}
	
	/**
	 * Increase the number of achievements of the specific campaign, which achievement that user just unlocked.
	 * <p>in user_campaign relationship table, two fields "camp_total" and "camp_num" that the first one stored the
	 * achievements number this campaign has, and the second one is the quantity of achievements that this user unlocked.
	 * this function is invoked after user triggered one achievement first time and add the number plus one to the table's
	 * "camp_num" field and also has functionality to judge whether "camp_total" equals to "camp_num", if so, make the
	 * unlock_flag to 1, which means this user could join in this campaign.
	 * @param username	user name
	 * @param camp_id	camp id
	 * @return			true/false
	 */
	public boolean addAchiUnlockedNumToUserCamp(String username, int camp_id) {
		log.debug("Entering addAchiUnlockedNumToUserCamp(username = {}, camp_id = {})", username, camp_id);
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			session.beginTransaction();
			// get campaign object by using campaign id
			Query query = session.createQuery("from Campaign camp where camp.id = :camp_id")
					.setParameter("camp_id", camp_id); 
			camp = (Campaign) query.uniqueResult(); 
			
			// get user object and user id by using user name
			query = session.createQuery("from User user where user.username = :username")
					.setParameter("username", username); 
			user = (User) query.uniqueResult(); 
			int user_id = user.getId(); 
			
			// if user has unlocked this campaign's achievement before, just make camp_num add 1
			if (user.getCamps().contains(camp)) {
				session.createSQLQuery("update t_user_campaign set camp_num=camp_num+1 where user_id = :user_id and camp_id = :camp_id")
				       .setParameter("user_id", user_id)
				       .setParameter("camp_id", camp_id)
				       .executeUpdate();
				session.getTransaction().commit();
			// if user does not have this campaign record, add a new campaign
			} else {
				user.getCamps().add(camp);
				session.save(user);
				session.getTransaction().commit();
				
				query = session.createSQLQuery("select count(achi_id) from t_campaign_achievement where camp_id = :camp_id")
							   .setParameter("camp_id", camp.getId()); 
				int camp_total = ((BigInteger) query.uniqueResult()).intValue(); 
				
				// count achievement num of this campaign and assign this value to campaign total field
				session.createSQLQuery("update t_user_campaign set camp_total = :camp_total where user_id = :user_id and camp_id = :camp_id")
			       	   .setParameter("camp_total", camp_total)
					   .setParameter("user_id", user_id)
			           .setParameter("camp_id", camp_id)
			           .executeUpdate();
				
			}
			// if camp_total equals camp_num, it means this user could join in this campaign, and then assign the unlock_flag to 1
			session.createSQLQuery("update t_user_campaign set unlock_flag = 1 where camp_total = camp_num and user_id = :user_id and camp_id = :camp_id")
		       .setParameter("user_id", user_id)
		       .setParameter("camp_id", camp_id)
		       .executeUpdate();
		} catch (Exception e) {
			e.printStackTrace();
			session.getTransaction().rollback();
			log.error("addAchiUnlockedNumToUserCamp Catch Exception: {}", e.getMessage());
			return false;
		} finally {
			session.close();
		}
		
		log.debug("Leaving addAchiUnlockedNumToUserCamp(): true");
		return true; 
	}

	/**
	 * Get unique user object by user name.
	 * @param username	user name
	 * @return 			user object
	 */
	public User getUserByUsername(String username) {
		log.debug("Entering getUserByUsername(username = {})", username);
		
		Session session = null; 
		try {
//			session = SessionFactoryUtil.getSessionFactory().openSession(); 
			session = sessionFactory.openSession();
			Query query = session.createQuery("from User user where user.username = :username")
					.setParameter("username", username); 
			user = (User) query.uniqueResult(); 
		} catch (Exception e) {
			e.printStackTrace();
			log.error("getUserByUsername Catch Exception: {}", e.getMessage());
		} finally {
//			SessionFactoryUtil.closeSession(session);
			session.close();
		}
		
		log.debug("Leaving getUserByUsername(): user_id = {}", user.getId());
		return user; 
	}
	
	/**
	 * Get user List by input string for fuzzy query.
	 * @param inputString	user name
	 * @return 				User List
	 */
	@SuppressWarnings("unchecked")
	public List<User> getUserListByInputString(String inputString) {
		log.debug("Entering getUserListByInputString(inputString = {})", inputString);
		
		List<User> userList = new ArrayList<User>();
		Session session = null; 
		try {
//			session = SessionFactoryUtil.getSessionFactory().openSession(); 
			session = sessionFactory.openSession();
			Query query = session.createQuery("from User user where user.username like :username")
					.setParameter("username", "%" + inputString + "%"); 
			userList = query.list(); 
		} catch (Exception e) {
			e.printStackTrace();
			log.error("getUserListByInputString Catch Exception: {}", e.getMessage());
		} finally {
//			SessionFactoryUtil.closeSession(session);
			session.close();
		}
		
		log.debug("Leaving getUserListByInputString(): userList.size = {})", userList.size());
		return userList; 
	}
	
	/**
	 * Get all user objects with List format
	 * @return user List
	 */
	@SuppressWarnings("unchecked")
	public List<User> getUserList() {
		log.debug("Entering getUserList()");
		List<User> userList = new ArrayList<User>(); 
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			Query query = session.createQuery("from User user order by user.score asc");
			userList = query.list(); 
		} catch (Exception e) {
			e.printStackTrace();
			log.error("getUserList Catch Exception: {}", e.getMessage());
		} finally {
			session.close();
		}
		
		log.debug("Leaving getUserList(): userList.size = {}", userList.size());
		return userList; 
	}
	

	/**
	 * Add score to the user when user unlocked the achievement in the first time.
	 * @param username		user name
	 * @param achi_score	achievement's score points
	 * @return				true/false
	 */
	public boolean addScoreToUser(String username, long achi_score) {
		log.debug("Entering addScoreToUser(username = {}, achi_score = {})", username, achi_score);
		Session session = null; 
		try {
			session = sessionFactory.openSession();
			session.beginTransaction();
			session.createSQLQuery("update t_user set score = score + :achi_score where username = :username")
		       .setParameter("achi_score", achi_score)
		       .setParameter("username", username)
		       .executeUpdate();
			session.getTransaction().commit();
		} catch (Exception e) {
			session.getTransaction().rollback();
			e.printStackTrace();
			log.error("addScoreToUser Catch Exception: {}", e.getMessage());
			return false;
		} finally {
			session.close();
		}
		
		log.debug("Leaving addScoreToUser() : true");
		return true;
	}
	
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	@Resource
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	public User getUser() {
		return user;
	}

	@Resource
	public void setUser(User user) {
		this.user = user;
	}

	public Achievement getAchi() {
		return achi;
	}


	@Resource
	public void setAchi(Achievement achi) {
		this.achi = achi;
	}

	public Badge getBadge() {
		return badge;
	}

	@Resource
	public void setBadge(Badge badge) {
		this.badge = badge;
	}

	public Campaign getCamp() {
		return camp;
	}

	@Resource
	public void setCamp(Campaign camp) {
		this.camp = camp;
	}
	
	
}
